[Transcribe] 動画内でしゃべった言葉をテキスト検索可能にする

[Transcribe] 動画内でしゃべった言葉をテキスト検索可能にする

Clock Icon2024.10.10

Introduction

最近もろもろの事情があり、よくAmazon Trascribeを触ってます。
この前、「動画内でしゃべったフレーズで検索して、その場面から再生する」
という機能をデモ実装しました。
これはTranscribeで動画の文字起こしを行い、その結果をベクトルDBに登録し、
キーワード検索を実装して実現しています。

これがあれば、「あの話してたのどのへんだっけ?」って思って
動画内をあちこち探すこともなくなります。

今回は動画内キーワード検索のデモアプリ構築方法について紹介します。

Environment

  • Node.js: v22.5.1

AWSアカウントはセットアップ済みで、Transcribeが使用可能な状態とします。
また、OpenAIのアクセストークンも取得済みとします。

Setup

今回はNode.jsを使って動画内検索のデモを作成します。
ディレクトリを作成してnpm initします。

mkdir vsearch-demo
cd vsearch-demo
npm init -y

必要なパッケージのインストールを行います。
今回は簡単に、fastifyを使って簡単に実装します。
その他必要なパッケージをインストール。

npm install fastify @fastify/multipart @fastify/static @fastify/view ejs dotenv @aws-sdk/client-s3 @aws-sdk/client-transcribe node-fetch gpt-3-encoder lowdb

環境変数設定用のファイルを作成します。
.envファイルを下記内容で作成。
Open AIのトークン取得とS3バケット作成後、
下記内容を設定しましょう。

AWS_REGION=your_aws_region
S3_BUCKET_NAME=your_s3_bucket_name
OPENAI_API_KEY=your_openai_api_key

Try

今回は動画アップロード用画面と動画再生&検索用画面を作成します。
とりあえずのデモとして実装したので、
無駄な処理とかいろいろありますがご勘弁を。

では、アプリの主要な機能を見ていきましょう。
リポジトリはこちらにあるので参照してください。

動画アップロードからの処理の流れは以下の通りです。

1.動画ファイルをS3にアップロード
2.文字起こし依頼
3.終わったらメタデータファイルをダウンロード
4.ベクトルデータの生成と保存

動画のアップロードと文字起こし

services/transcription.jsではアップロードしたファイルを
Amazon Transcribeで文字起こししてます。

async function transcribeAudio(fileName) {
  const jobName = `transcribe_${Date.now()}`;
  const mediaFileUri = `s3://${process.env.S3_BUCKET_NAME}/${fileName}`;

  const params = {
    TranscriptionJobName: jobName,
    LanguageCode: 'ja-JP',
    MediaFormat: 'mp4',
    Media: {
      MediaFileUri: mediaFileUri,
    },
    OutputBucketName: process.env.S3_BUCKET_NAME,
  };

  try {
    await transcribeClient.send(new StartTranscriptionJobCommand(params));
    await waitForTranscriptionCompletion(jobName, fileName);
  } catch (err) {
    console.error('文字起こし中にエラーが発生しました:', err);
    throw err;
  }
}

↑の関数は、S3にアップロードされた動画ファイルに対してAWS Transcribeジョブを開始します。waitForTranscriptionCompletionで、ジョブの完了を待ち、結果を取得します。

ベクトルデータの生成

config/database.jsのgenerateEmbeddingsでは文字起こし結果のファイルをパースして
ベクトル化したデータをlowdbに登録しています。

async function generateEmbeddings(fileName) {

・・・・

  const items = transcriptEntry.transcript.results.items;
  if (!items || !Array.isArray(items)) {
    console.log('Invalid items array in transcript');
    return;
  }

  // 埋め込みの生成と保存
  for (let chunk of chunks) {
    const text = chunk.map(item => item.alternatives[0].content).join(' ');
    const start_time = parseFloat(chunk[0].start_time);
    const end_time = parseFloat(chunk[chunk.length - 1].end_time);
    try {
      const embedding = await getEmbedding(text);
      db.get('embeddings').push({
        fileName,
        text,
        embedding,
        start_time,
        end_time,
      }).write();
    } catch (error) {
      console.error('Error generating embedding for chunk:', error);
    }
  }
}

services/embedding.jsgetEmbeddingでは、OpenAIのAPIを使用してテキストの埋め込みベクトルを生成します。

const { encode, decode } = require('gpt-3-encoder');

const MAX_TOKENS = 5000;

async function getEmbedding(text) {
  if (!text || text.trim().length === 0) {
    throw new Error('Text is empty');
  }

  // トークン数のチェックと分割
  const tokens = encode(text);
  if (tokens.length > MAX_TOKENS) {
    text = decode(tokens.slice(0, MAX_TOKENS));
  }

  return await getEmbeddingForChunk(text);
}

これで動画検索の準備が整いました。

ベクトル検索

動画アップロード処理完了後、動画再生&検索用画面にアクセスして
動画内のキーワード検索を実施します。

検索は、config/database.jssearchTranscriptsで検索を行います。

async function searchTranscripts(keyword, fileName, options = {}) {
  const embeddings = db.get('embeddings').filter({ fileName }).value();

  if (options.useEmbedding) {
    const keywordEmbedding = await getEmbedding(keyword);
    results = embeddings.map(embeddingEntry => ({
      text: embeddingEntry.text,
      start_time: embeddingEntry.start_time,
      end_time: embeddingEntry.end_time,
      similarity: cosineSimilarity(keywordEmbedding, embeddingEntry.embedding),
    }));

    results = results.filter(result => result.similarity >= similarityThreshold)
                     .sort((a, b) => b.similarity - a.similarity);
  } else {
    // ...
  }

  return results;
}

この関数は、キーワードと文字起こしテキストのベクトル間の類似度を計算し、
最も類似度の高い結果を返します。
※このあたりはChatGPT氏に聞いた、とりあえずな実装です

4. データの永続化と検索機能の実装

config/database.jsファイルは、アプリケーションのデータ永続化と検索機能を実装しています。
デモでは、lowdbを使用してベクトルデータの保存と検索を実施します。

const low = require('lowdb');
const FileSync = require('lowdb/adapters/FileSync');

// データベースの初期化
const adapter = new FileSync('db.json');
const db = low(adapter);

function initDatabase() {
  db.defaults({ transcripts: [], embeddings: [] }).write();
}

db.jsonファイルをデータストアとして使用するlowdbインスタンスを初期化しています。

テキストのベクトル化とOpenAI API

デモアプリでは、文字起こしした結果のデータをベクトル化することで、
意味的に類似した内容を検索することが可能になります。

今回はかなり雑なベクトル化と検索ですが、
とりあえずそれっぽいワードもヒットしてるので良しとする。

OpenAI APIの使用

アプリでは、OpenAIのAPIを使用して文字起こし結果テキストのベクトル化を行っています。

async function getEmbedding(text) {
  // ...
  const response = await fetch('https://api.openai.com/v1/embeddings', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      input: chunk,
      model: "text-embedding-ada-002"
    })
  });
  // ...
}

text-embedding-ada-002は、OpenAIのテキスト埋め込みモデルです。
高品質、低価格でテキストを高次元のベクトル空間に変換してくれます。

動かしてみる

動画のサンプルとして、ここにある4K8K放送の紹介動画をつかってみました。

アプリを起動して、アップロードから検索まで試してみましょう。

% export OPENAI_API_KEY=<OpenAIのアクセストークン>

% cd vsearch-demo
% node app.js
・
・
{"level":30,"time":1728543420383,"pid":28861,"hostname":"local","msg":"サーバーが起動しました: 3300"}

ブラウザでlocalhostにアクセスして↑のmp4をアップロードします。
結構時間がかかるのでコンソールログをみながらじっと待つ。

vsearch-2.png

アップロードとベクトル化がおわったら検索してみましょう。
「インターネット」と検索すると、それをしゃべっている箇所(またはそれに近いもの)の一覧が表示されます。
それをクリックすると、その場面から動画が再生されます。

vsearch-1.png

Summary

今回はTranscribeを使って動画内でのキーワード検索をやってみました。
ベクトル化や検索方法については実際はもっと考慮する必要がありますが、

一応30分くらいの動画(弊社の昼礼)でためしてみてもとりあえず検索は動いたので、
それなりに使えそうです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.